package com.nami.framework.config;

import com.nami.common.property.SecurityProperty;
import com.nami.framework.security.handler.*;
import com.nami.framework.security.service.UserDetailsServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;

/**
 * Security 配置
 *
 * @author C
 */
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig<S extends Session> {

    /**
     * 系统数据信息
     */
    private final SecurityProperty securityProperty;

    /**
     * 401 未知账号处理
     */
    private final SecureityAuthenticationEntryPoint secureityAuthenticationEntryPoint;

    /**
     * 403 无权限处理
     */
    private final SecurityAccessDeniedHandler securityAccessDeniedHandler;

    /**
     * 自定义退出成功处理类
     */
    private final SecurityLogoutSuccessHandler securityLogoutSuccessHandler;

    /**
     * springSession处理
     */
    private final FindByIndexNameSessionRepository<S> findByIndexNameSessionRepository;

    /**
     * 登录实现类
     */
    private final UserDetailsServiceImpl userDetailsService;


    /**
     * session并发处理控制类
     */
    private final SecuritySessionExpiredHandler securitySessionExpiredHandler;


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        log.info("同时允许登录的用户个数: {}", securityProperty.getMaximum());
        http.authorizeRequests(auhtor -> auhtor
                // 这里配置的接口不需要验证
                .antMatchers(securityProperty.getOpenApi()).permitAll()
                // 其它接口都需要经过验证
                .anyRequest().authenticated())
            .httpBasic(basic -> basic
                // 配置未知账号处理类
                .authenticationEntryPoint(secureityAuthenticationEntryPoint))
            .exceptionHandling(exception -> exception
                // 配置权限不足处理类
                .accessDeniedHandler(securityAccessDeniedHandler))
            // 退出时操作
            .logout(logout -> logout.deleteCookies("JSESSIONID").logoutSuccessHandler(securityLogoutSuccessHandler))
            // session管理器
            .sessionManagement(sessionManagement -> sessionManagement
                // 在需要使用到session时才创建session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                // 当用户成功通过身份验证时，将创建一个新会话并将旧会话值移动到新会话。（默认并适用于大多数情况）
                .sessionFixation().migrateSession()
                // 每个用户在系统中的最大session数量
                .maximumSessions(securityProperty.getMaximum())
                // 是否保留已经登录的用户；为true，新用户无法登录；为 false，旧用户被踢出
                // 如果设置为 true，表示某用户达到最大会话并发数后，新会话请求会被拒绝登录；如果设置为 false，表示某用户达到最大会话并发数后，新会话请求访问时，其最老会话会在下一次请求时失效并根据 expiredUrl() 或者 expiredSessionStrategy() 方法配置的会话失效策略进行处理，默认值为 false。
                .maxSessionsPreventsLogin(true)
                // session 并发数据的维护将由 SpringSessionBackedSessionRegistry 来完成
                // 设置所要使用的 sessionRegistry，默认配置的是 SessionRegistryImpl 实现类。
                .sessionRegistry(sessionRegistry())
                // 用户被踢出操作
                // 如果某用户达到最大会话并发数后，新会话请求访问时，其最老会话会在下一次请求中失效并按照该策略处理请求。注意如果本方法与 expiredUrl() 同时使用，优先使用 expiredUrl() 的配置。
                .expiredSessionStrategy(securitySessionExpiredHandler));
        // 取消跨站请求伪造防护
        http.csrf().disable();
        // 防止iframe 造成跨域
        http.headers().frameOptions().disable();
        return http.build();
    }


    /**
     * spring-session管理session
     * SpringSessionBackedSessionRegistry 是session为Spring Security提供的
     * 主要用于集群环境下控制会话并发的会话注册表实现
     */
    @Bean
    public SpringSessionBackedSessionRegistry<S> sessionRegistry() {
        return new SpringSessionBackedSessionRegistry<>(findByIndexNameSessionRepository);
    }

    /**
     * 加密方式
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 解决 无法直接注入 AuthenticationManager
     */
    @Bean
    AuthenticationManager authenticationManager() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(bCryptPasswordEncoder());
        provider.setUserDetailsService(userDetailsService);
        return new ProviderManager(provider);
    }
}
